﻿/*
 * Copyright 2011 Edward L. Stauff.                  Contact: <EdStauff@gmail.com>.
 * 
 * This file, "TextWriterRepeater.cs", is part of the "Stauffware.Common" library.
 * "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common" sources.  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Stauffware.Common.Core;

namespace Stauffware.Common.IO
{
	/// <summary>
	/// Identifies a TextWriter operation.
	/// </summary>
	public enum TextWriterOperation
	{
		/// <summary>
		/// TextWriter.Close
		/// </summary>
		Close,

		/// <summary>
		/// TextWriter.Flush
		/// </summary>
		Flush,

		/// <summary>
		/// one of the TextWriter.Write methods
		/// </summary>
		Write
	}

	//#################################################################################
	/// <summary>
	/// A TextWriter that repeats its output to a list of recipient TextWriters. 
	/// </summary>

	public class RepeatingTextWriter : TextWriter
	{
		/// <summary>
		/// Delegate for handling an exception thrown by a recipient TextWriter.
		/// </summary>
		/// <param name="tw">
		///		The recipient TextWriter that throw the exception (not this TextWriterRepeater).
		/// </param>
		/// <param name="op">The operation that was being performed.</param>
		/// <param name="x">The exception that was thrown.</param>
		/// <returns>
		///		True = ignore the exception and continue. 
		///		False = allow the exception to continue being thrown.
		/// </returns>
		public delegate bool ExceptionHandler (TextWriter tw, TextWriterOperation op, Exception x);

		/// <summary>
		/// When an exception is thrown by a recipient TextWriter, if this handler is
		/// non-null, it is called to handle the exception.  If this handler is null,
		/// then exceptions thrown by recipient TextWriters are not caught by 
		/// TextWriterRepeater methods.
		/// </summary>
		public ExceptionHandler ExceptionRaised; 

		/// <summary>
		/// The list of recipients.
		/// </summary>
		public readonly ReadOnlyList<TextWriter> Recipients;

		/// <summary>
		/// The Encoding to return from the Encoding property.  For more details,
		/// see <see cref="Encoding"/>.
		/// </summary>
		public Encoding EncodingToReturn = null;

		/// <summary>
		/// The value to return from the FormatProvider property.
		/// </summary>
		public IFormatProvider FormatProviderToReturn = null;

		private readonly List<TextWriter> _recipients;		// the real list

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructs a TextWriterRepeater that doesn't send its output anywhere (yet).
		/// </summary>

		public RepeatingTextWriter ( )
		{
			this._recipients = new List<TextWriter>();
			this.Recipients = new ReadOnlyList<TextWriter>(this._recipients);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructs a TextWriterRepeater that sends its output to the given TextWriter(s).
		/// Duplicates are not added.
		/// Fails an assertion if adding one of the given TextWriters would result in a  
		/// circular chain of containment.
		/// </summary>

		public RepeatingTextWriter (params TextWriter[] writers)
			: this()
		{
			this.Add(writers);
		}

		//-----------------------------------------------------------------------------

		#region Methods for managing the list of recipients

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Adds one or more TextWriters to the list of recipients.
		/// Duplicates are not added.
		/// Fails an assertion if adding one of the given TextWriters would result in a  
		/// circular chain of containment.
		/// </summary>
		/// <returns>
		///		True if all the given TextWriters were added, or false if one or more 
		///		TextWriters were already in the list (the others are added).
		/// </returns>

		public bool Add (params TextWriter[] writers)
		{
			bool retValue = true;

			foreach (TextWriter tw in writers)
				if (!this.AddOne(tw))
					retValue = false;

			return retValue;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Adds one TextWriter to our list of TextWriters.  
		/// Fails an assertion if adding the given TextWriter would result in a circular 
		/// chain of containment.
		/// </summary>
		/// <param name="tw">the TextWriter to add</param>
		/// <returns>
		///		True on success, false if the given TextWriter is already in our list 
		///		(directly or indirectly).
		/// </returns>

		private bool AddOne (TextWriter tw)
		{
			Require.NotNull(tw);
			Require.Condition(tw != this, "a TextWriterRepeater may not contain itself");

			if (tw is RepeatingTextWriter)
				Require.Condition(!((RepeatingTextWriter) tw).Contains(this, true),
					"a TextWriterRepeater may not contain itself, even indirectly");

			if (this.Contains(tw, true))
				return false;

			this._recipients.Add(tw);
			return true;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Removes the given TextWriter from the list of recipients,
		/// but does not close it.
		/// </summary>
		/// <param name="textWriter">the TextWriter to remove</param>
		/// <param name="recurse">
		///		whether to recursively check recipients that are themselves TextWriterRepeaters
		/// </param>
		/// <returns>true if the TextWriter was removed, false if it wasn't found</returns>

		public bool Remove (TextWriter textWriter, bool recurse=false)
		{
			if (this._recipients.Remove(textWriter))
				return true;
			if (recurse)
				foreach (TextWriter tw in this._recipients)
					if (tw is RepeatingTextWriter)
						if (((RepeatingTextWriter) tw).Remove(textWriter, true))
							return true;
			return false;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns whether the given TextWriter is in the list of recipients.
		/// </summary>
		/// <param name="textWriter">the TextWriter to look for</param>
		/// <param name="recurse">
		///		whether to recursively check recipients that are themselves TextWriterRepeaters
		/// </param>
		/// <returns>true if found, false if not</returns>

		public bool Contains (TextWriter textWriter, bool recurse=false)
		{
			foreach (TextWriter tw in this._recipients)
			{
				if (tw == textWriter)
					return true;
				if (recurse && tw is RepeatingTextWriter)
					if (((RepeatingTextWriter) tw).Contains(textWriter, true))
						return true;
			}

			return false;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Removes all recipients from the list.
		/// </summary>
		/// <param name="close">whether to close the recipients before removing them</param>

		public void Clear (bool close)
		{
			if (close)
				foreach (TextWriter tw in this._recipients)
					tw.Close();
			this._recipients.Clear();
		}

		//-----------------------------------------------------------------------------

		#endregion

		//-----------------------------------------------------------------------------

		private delegate void TextWriterMethod(TextWriter tw);

		// invokes the given delegate for each recipient
		private void Repeat (TextWriterOperation op, TextWriterMethod dlgt)
		{
			if (this.ExceptionRaised == null)
				foreach (TextWriter tw in this._recipients)
					dlgt(tw);
			else
				foreach (TextWriter tw in this._recipients)
				{
					try
					{
						dlgt(tw);
					}
					catch (Exception x)
					{
						if (!this.ExceptionRaised(tw, op, x))
							throw;
					}
				}
		}

		#region Overridden TextWriter Properties & Methods

		//-----------------------------------------------------------------------------
		/// <summary>
		/// If EncodingToReturn is not null, then that value returned.  Otherwise, the
		/// the Encodings of the recipients are examined.  If they all have the same 
		/// Encoding (not counting null Encodings, which are ignored), then that Encoding
		/// will be returned.  If multiple non-null Encodings are found in the recipients,
		/// then an exception is thrown.  If no Encodings at all are found, then null is
		/// returned.
		/// </summary>
		/// <remarks>
		/// This may not provide sufficient flexibility.  Similar issues with FormatProvider
		/// and NewLine.  Possible additional approaches:
		/// 1. Require that all recipient TextWriters have our Encoding.
		/// 2. Change the encoding on all recipient TextWriters to have our encoding.
		///		(Can we do this?  We can for NewLine, anyway.)
		/// </remarks>

		public override Encoding Encoding
		{	
			get 
			{
				if (this.EncodingToReturn != null)
					return this.EncodingToReturn;

				System.Text.Encoding enc = null;
				foreach (TextWriter tw in this.Recipients)
				{
					try
					{
						if (enc == null)
							enc = tw.Encoding;
						else if (enc != tw.Encoding)
							throw new Exception("RepeatingTextWriter recipients have different Encodings, including " +
									enc.ToString() + " and " + tw.Encoding + ".");
					}
					catch (NotSupportedException)
					{
						// Fine, be like that.
					}
				}
				return enc;
			}	
		}

		//-----------------------------------------------------------------------------

		/// <summary>
		/// Returns this.FormatProviderToReturn.
		/// </summary>
		public override IFormatProvider FormatProvider
		{ get { return this.FormatProviderToReturn; } }

		/// <summary>
		/// Not supported; values could be different for each recipient.
		/// </summary>
		public override string NewLine
		{
			get { throw new NotSupportedException(); }
			set { throw new NotSupportedException(); }
		}

		//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

		/// <summary>
		/// Closes all the recipients.
		/// </summary>
		public override void Close ( )
		{	this.Repeat(TextWriterOperation.Close, delegate(TextWriter tw) { tw.Close(); });	}

		/// <summary>
		/// Flushes all the recipients.
		/// </summary>
		public override void Flush ( )
		{	this.Repeat(TextWriterOperation.Flush, delegate(TextWriter tw) { tw.Flush(); });	}

		/// Overridden mainly for debugging.
		public override string ToString ( )
		{	return this.GetType().FullName + "{" + this._recipients.Count.ToString() + "}";	}

		/// <summary>
		/// Writes a character to all the recipients.
		/// </summary>
		public override void Write (char value)
		{	this.Repeat(TextWriterOperation.Write, delegate(TextWriter tw) { tw.Write(value); }); }

		/// <summary>
		/// Writes a string to all the recipients.
		/// </summary>
		public override void Write (string value)
		{	this.Repeat(TextWriterOperation.Write, delegate(TextWriter tw) { tw.Write(value); });	}

		/// <summary>
		/// Writes a blank line to all the recipients.
		/// </summary>
		public override void WriteLine ( )
		{	this.Repeat(TextWriterOperation.Write, delegate(TextWriter tw) { tw.WriteLine(); }); }

		/// <summary>
		/// Writes a line of text to all the recipients.
		/// </summary>
		public override void WriteLine (string value)
		{	this.Repeat(TextWriterOperation.Write, delegate(TextWriter tw) { tw.WriteLine(value); }); }

		// NOTE: it's not clear what advantage there is,  
		//		if any, to overriding the other methods.

		#endregion

		//-----------------------------------------------------------------------------
	}
}
